package com.techyuan.intellij.tomcat.conf;

import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.JavaCommandLineState;
import com.intellij.execution.configurations.JavaParameters;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.util.io.ZipUtil;
import com.techyuan.intellij.tomcat.runner.RunningConfiguration;
import com.techyuan.intellij.tomcat.utils.PluginUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.incremental.ExternalProcessUtil;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Map;

/**
 * derek.tian
 */
public class TomcatCommandLineState extends JavaCommandLineState {

    private static final String TOMCAT_MAIN_CLASS = "org.apache.catalina.startup.Bootstrap";
    private TomcatRunConfiguration configuration;

    protected TomcatCommandLineState(@NotNull ExecutionEnvironment environment) {
        super(environment);
    }

    protected TomcatCommandLineState(ExecutionEnvironment environment, TomcatRunConfiguration configuration) {
        this(environment);
        this.configuration = configuration;
    }

    @Override
    protected JavaParameters createJavaParameters() throws ExecutionException {
        try {
            RunningConfiguration runningConf = createRunningConf(configuration);
            prepareTomcatEnvironment(runningConf);
            return  doCreateJavaParameters(runningConf);
        } catch (ExecutionException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new ExecutionException("create java parameters exception!", ex);
        }
    }

    private JavaParameters doCreateJavaParameters(RunningConfiguration runningConf) throws ExecutionException {
        String vmOptions = configuration.getVmOptions();
        Map<String, String> envOptions = configuration.getEnvOptions();

        Project project = this.configuration.getProject();
        JavaParameters javaParams = new JavaParameters();
        ProjectRootManager manager = ProjectRootManager.getInstance(project);
        javaParams.setJdk(manager.getProjectSdk());
        javaParams.setDefaultCharset(project);
        javaParams.setMainClass(TOMCAT_MAIN_CLASS);
        javaParams.getProgramParametersList().add("start");
        addBinFolder(runningConf.getTomcatInstallationPath(), javaParams);
        addLibFolder(runningConf.getTomcatInstallationPath(), javaParams);
        javaParams.setWorkingDirectory(runningConf.getWorkPath().toFile());
        javaParams.setPassParentEnvs(configuration.getPassParentEnvironmentVariables());
        javaParams.getVMParametersList().addParametersString(vmOptions);
        if (envOptions != null) {
            javaParams.setEnv(envOptions);
        }
        return javaParams;
    }

    @Nullable
    @Override
    protected ConsoleView createConsole(@NotNull Executor executor) {
        return new TomcatServerConsoleView(configuration);
    }

    /**************** 准备tomcat相关的环境 ********************/

    private RunningConfiguration createRunningConf(TomcatRunConfiguration configuration) throws ExecutionException {
        RunningConfiguration runningConf = new RunningConfiguration();
        runningConf.setTomcatInstallationPath(Paths.get(configuration.getTomcatInfo().getPath()));

        Module module = configuration.getModule(configuration.getProject());
        if (module == null) {
            throw new ExecutionException("deployment module must be selected!");
        }
        runningConf.setModule(module);

        //检查端口占用
        if (PluginUtils.isPortUsing(NumberUtils.toInt(configuration.getPort()))) {
            throw new ExecutionException("server port "+ configuration.getPort() +"is used!");
        }

        if (PluginUtils.isPortUsing(NumberUtils.toInt(configuration.getAdminPort()))) {
            throw new ExecutionException("admin port "+ configuration.getAdminPort() +"is used!");
        }
        return runningConf;
    }

    /**
     * 准备tomcat相关的环境
     */
    private RunningConfiguration prepareTomcatEnvironment(RunningConfiguration runningConf) throws Exception {
        //1.确定root目录
        Path workPath = prepareWorkPath(runningConf.getModule());
        runningConf.setWorkPath(workPath);


        //2.监理root目录下的自己目录
        runningConf.setWarPath(runningConf.getWorkPath().resolve("app"));

        runningConf.setConfPath(workPath.resolve("conf"));
        runningConf.getConfPath().toFile().mkdirs();
        runningConf.getWorkPath().resolve("work").toFile().mkdirs();
        runningConf.getWorkPath().resolve("logs").toFile().mkdirs();

        FileUtil.copyFileOrDir(runningConf.getTomcatInstallationPath().resolve("conf").toFile(), runningConf.getConfPath().toFile());

        //3.找到war包，将其copy到webapps目录
        //TODO 通过其他方式获取war包或展开信息的数据
        File warFile = findWarInModule(runningConf.getModule());

        ZipUtil.extract(warFile, runningConf.getWarPath().toFile(), null, true);
        //4.配置server.xml
        String contextPath = configuration.getContextPath();
        updateServerConf(contextPath, runningConf);
        return runningConf;
    }

    /**
     * 1.修改context，设置docBase和contextPath
     * @param contextPath
     * @param runningConf
     * @throws Exception
     */
    private void updateServerConf(String contextPath, RunningConfiguration runningConf) throws Exception {
        Path serverXml = runningConf.getConfPath().resolve("server.xml");

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        org.w3c.dom.Document doc = builder.parse(serverXml.toUri().toString());
        XPathFactory xPathfactory = XPathFactory.newInstance();
        XPath xpath = xPathfactory.newXPath();
        XPathExpression exprConnector = xpath.compile("/Server/Service[@name='Catalina']/Connector[@protocol='HTTP/1.1']");
        XPathExpression expr = xpath.compile("/Server/Service[@name='Catalina']/Engine[@name='Catalina']/Host");
        XPathExpression exprConnectorShutdown = xpath.compile("/Server[@shutdown='SHUTDOWN']");

        Element portE = (Element) exprConnector.evaluate(doc, XPathConstants.NODE);
        Element portShutdown = (Element)exprConnectorShutdown.evaluate(doc, XPathConstants.NODE);
        Node hostNode = (Node) expr.evaluate(doc, XPathConstants.NODE);

        //1.设置tomcat的 contextPath和web项目的关系
        Element contextE = doc.createElement("Context");
        contextE.setAttribute("docBase", runningConf.getWarPath().toFile().getAbsolutePath());
        contextE.setAttribute("path", (contextPath.startsWith("/") ? "" : "/") + contextPath);
        hostNode.appendChild(contextE);


        //2.设置port
        portE.setAttribute("port", configuration.getPort());

        //3.设置shutdown
        portShutdown.setAttribute("port", configuration.getAdminPort());


        Source source = new DOMSource(doc);
        StreamResult result = new StreamResult(new OutputStreamWriter(new FileOutputStream(serverXml.toFile()),
                StandardCharsets.UTF_8));
        Transformer xformer = TransformerFactory.newInstance().newTransformer();
        xformer.transform(source, result);
    }

    private void addBinFolder(Path tomcatInstallation, JavaParameters javaParams) throws ExecutionException {
        // Dynamically adds the tomcat jars to the classpath
        Path binFolder = tomcatInstallation.resolve("bin");
        if (!Files.exists(binFolder)) {
            throw new ExecutionException("The Tomcat installation configured doesn't contains a bin folder");
        }
        String[] jars = binFolder.toFile().list((dir, name) -> name.endsWith(".jar"));

        assert jars != null;
        for (String jarFile : jars) {
            javaParams.getClassPath().add(binFolder.resolve(jarFile).toFile().getAbsolutePath());
        }
    }

    private void addLibFolder(Path tomcatInstallation, JavaParameters javaParams) throws ExecutionException {
        // add libs folder
        Path libFolder = tomcatInstallation.resolve("lib");
        if (!Files.exists(libFolder)) {
            throw new ExecutionException("The Tomcat installation configured doesn't contains a lib folder");
        }
        String[] jars = libFolder.toFile().list((dir, name) -> name.endsWith(".jar"));

        assert jars != null;
        for (String jarFile : jars) {
            javaParams.getClassPath().add(libFolder.resolve(jarFile).toFile().getAbsolutePath());
        }
    }

    private File findWarInModule(Module module) throws ExecutionException {
        String moduleRootPath = ModuleUtilCore.getModuleDirPath(module).replace("/.idea/modules", "");
        String[] findDirPaths = new String[] { "build", "out", "target", "" };

        for (String findDir : findDirPaths) {
            File findRoot = Paths.get(moduleRootPath, findDir).toFile();
            if (!findRoot.exists()) {
                continue;
            }
            Collection<File> files = FileUtils.listFiles(Paths.get(moduleRootPath, findDir).toFile(), new String[] {"war"}, true);
            if (!files.isEmpty()) {
                return files.iterator().next();
            }
        }

        throw new ExecutionException("cannot find war file in [" + StringUtils.join(findDirPaths) + "]");
    }

    private Path prepareWorkPath(Module module) throws ExecutionException {
        String userHome = System.getProperty("user.home");
        Path parentPath = Paths.get(userHome, ".SmartTomcat", module.getProject().getName());


        Path workPath = parentPath.resolve(module.getName() + "_" +
                DigestUtils.md5Hex(configuration.getPort() + ":" + configuration.getAdminPort()));
        workPath.toFile().delete();
        workPath.toFile().mkdirs();

        return workPath;
    }
}
